理解 Javascript 的非同步操作:Event Loop

1
2
setTimeout(function(){console.log('delay 0 sec')}, 0)
console.log('Hello!')

上述範例直覺可能會覺得輸出為 delay 0 sec –> Hello,但結果為 Hello –> delay 0 sec。為什麼?因為有事件循環 (Event Loop) 的參與,這個機制的目的主要想讓 Javascript 提供並行 (Concurrency) 運作。

先快速總結一下自己最後的理解,Javascript 在並行中的非同步效果需要 Stack、Callback Queue 和 WebAPIs 三方合作;它不是 Javascript Engine 自身非同步,而是利用 Callback Queue 來避免 Stack 依序執行程式造成可能發生的阻塞問題,例如非同步的設計讓使用者在使用瀏覽器 (Browser) 的過程中能夠看到更新的頁面 (render) 又同時能得到點擊滑鼠時的相應回饋。

以下這些概念可能需要依序建立。

Javascript 的執行環境:Call Stack & Heap Memory

Javascript 是單執行緒 (single threaded),也就是一次只執行一行指令,其實也可以直接說它的執行過程就是同步的;看到這裡可能會很直覺地反問,那為什麼我們可以使用各種監聽器 EventTarget.addEventListener()?這答案牽涉到整個頁面的運作不僅僅是 Javascript Engine,別忘了 Javascript Engine 是運作在瀏覽器上的。

圖片來源: https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
先回到 Javascript 本身,Javascript Engine 有兩個很重要的組成部分:Call Stack 和 Heap Memory。Call Stack 是一種資料結構,會幫忙紀錄整個 Javascript 的執行過程,而 function 的實際環境建構過程 (Excution Context) 發生在這個位置。當一個 function 被呼叫時,一個對應的 stack frame 會被創建出來,這個流程牽涉到直譯器的運作,執行環境會先被建立,過程牽涉到 scope chain 初始化、建立 function 的 argument 或 variable,進入執行階段後才開始真正的賦值。

總之這個 frame 建立時會被放到 Stack 的最上方,當該 function 執行完畢,則整個 context、也是 frame 便會從 Stack 中移除;Heap Memory 是記憶體分配的位置,和物件相關。

關於 Excution Context 可以參考這裡這裡。所以以一小段程式碼為例子:

1
2
3
4
5
6
7
8
9
10
function a() {
return 1
}
function b() {
a()
}
function c() {
b()
}
c()

補充一下,Stack 和 Queue 都是資料結構的一種,Stack 的特色是先進後出 (Last In First Out,LIFO),Queue 則是先進先出 (First In First Out,FIFO)。

PS:其實上面有兩個中文名詞都用到執行環境,一個是 runtime 一個是 execution context;但兩者存在著差別, runtime 想要描述的是牽涉到整個 JS 運作時的環境狀態,包含 WebAPIs 和 Javascript Engine。而 Excution Context 的環境則是指實際建構你的 function 環境、變數環境、作用域 (scope chain) 。

When you visit a website you do so within a web browser, like Chrome, Firefox, Edge, or Safari. Each browser has a JS Runtime Environment. In the environment are Web API’s that a developer can access to build a program.

Also in the runtime environment is a Javascript Engine that parses the code. Each browser has its own version of a JS engine. Chrome uses what it calls its V8 JS Engine and that is what we will analyze now.

WebAPIs?

剛剛其實已經提到整個頁面的運作需要 Javascript Engine 和 Browser 的合作。

先前總是在 Javascript 裡面寫到 EventTarget.addEventListener(),它並不是由 JS 提供 (真的是直到現在才突然理解以前教程上說的 “document 是由瀏覽器提供的方法” 這句話XD),它是來自瀏覽器的 WebAPI,所以除了 DOM 操作以外,AJAX、setTimeout 等等這些其實都是 WebAPI。

AJAX, the DOM tree, and other API’s, are not part of Javascript, they are just objects with properties and methods, provided by the browser and made available in the browser’s JS Runtime Environment.

Event Loop

所以說到這裡,到底 Event Loop 的機制是怎麼輔助 Javscript Engine 達到非同步的效果呢?我們已經知道在 Stack 會執行 function,但當這個 function 是 WebAPI 的時候,瀏覽器會執行這個 WebAPI,Stack 內的 function 會結束並離開,而 WebAPI 挾帶的 callback function 不會馬上被執行,callback function 會直到 WebAPI 觸發完條件 (可能是 setTimeout 的時間到了、可能是一個 click 事件等等) 時,被放入 Callback Queue。最後直到 Call Stack 被清空時,Callback Queue 才會進到 Stack 裡被執行。

圖片來源: https://medium.com/@Rahulx1/understanding-event-loop-call-stack-event-job-queue-in-javascript-63dcd2c71ecd
那可以說 Event Loop 是個 process,由這個 process 來觸發 Callback Queue,並且該 process 也會監控 Call Stack 是否已經空了,當 Stack 空了,則會從 Queue pop out 一個 process 並 push 進 Stack 裡面執行。

大神們怎麼說?

首先,可以參考 Philip Robert 解說 Event Loop (很重要!!必看!!!),涉及的觀念都說得很清楚,同時搭配很棒的範例;而 PJ 大大的筆記也做得很清晰,其實當能理解 Event Loop 的整體概念後,Phil 大大所描述的例子正是 “強化” 這個機制所帶來的效果,因此我們更能從影片裡真實感受到 Event Loop 運作模式。我簡單引用條列一下其中的例子:

1
2
3
4
5
6
7
console.log('hi')

setTimeout(function () {
console.log('there')
}, 0)

console.log('JSConfEU')

若不理解 Event Loop 的運作模式時,可能會認為輸出為 hi –> there –> JSConfEU ,但實際結果是 hi –> JSConfEU –> there。

因為 setTimeout 是 WebAPIs,我們的 callback function (這裡的 function(){console.log('there')}) 會在 WebAPIs 中等待 0 秒後放入 callback queue 裡,而 stack 裡的 function 都執行完畢後才會輪到 queue 裡的 function。

另外還有 Click Event 和 Multiple setTimeout,接下來的例子請搭配 Phil 的動畫食用,講解的非常清楚。

1
2
3
4
5
6
7
8
9
10
11
12
// Click Event //
console.log('Started')

$.on('button', 'click', function onClick () {
console.log('Clicked')
})

setTimeout(function onTimeout () {
console.log('Timeout Finished')
}, 5000)

console.log('Done')

執行結果來說,同樣是 Started –> Done,而 Click 事件以及 setTimeout 是依序進入 WebAPIs 之中,WebAPIs 裡的倒計時一結束就會放進 queue,確認 Done 已經出現後,才會顯示 “Timeout Finished”。同樣的,就算頁面一出現就馬上按滑鼠, “Clicked” 字樣的出現也必須等到 “Done” 出現後才會顯現喔!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Multiple setTimeout //
setTimeout(function timeout() {
console.log('hi')
}, 1000)

setTimeout(function timeout() {
console.log('hi')
}, 1000)

setTimeout(function timeout() {
console.log('hi')
}, 1000)

setTimeout(function timeout() {
console.log('hi')
}, 1000)

那這個範例想要給出的概念則是, timeout 並不能保證一定會在 1 秒後一定執行 callback function,而是說至少 1 秒的等待時間。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Synchronous
[1, 2, 3, 4].forEach(function (i) {
console.log(i)
delay()
})

// Asynchronous
function asyncForEach(array, cb) {
array.forEach(function () {
setTimeout(cb, 0)
})
}

asyncForEach([1, 2, 3, 4], function (i) {
console.log(i)
delay()
})

這是一個模擬瀏覽器 render 的情形,請同樣要搭配 Phil 大大的動畫食用喔!

渲染的優先權高於 callback function,若是同步的方式,則因為執行較久的函式 (也就是這裡的 delay()) 在 stack 中,需要等待所有 delay 結束才能渲染;但若是非同步,這些時長久的 function 會被放入 callback function,也因此給予了畫面渲染的機會。

上述僅拿出幾個當初一開始看到,我需要著墨一下的範例,還有更多範例都可以從提到的 reference 裡再研讀喔!

補充:同步 (Synchronize) 還是異步 (Asynchronize)

在這次找網路資源時,也順便糾正一下自己同步異步的觀念,這篇例子舉的挺生動啊。先前思考到這個點的時候,都沒有意識到一次處理多個指令這樣的講法和實際的運作存在落差。

過去認為的 “一次處理一個指令是同步” 和 “一次處理多個指令是異步” 的定義有點不太對,這個認為只是同步異步呈現出來的效果,可以理解為是客戶感受到的,但不是處理這些指令的對象實際的操作。

同步異步描述的是針對單一請求送出的處理模式,也就是同步實際上是 function 在執行時,你必須等待它處理完成,但若是異步,則是你把 function 交出去後,不用繼續等待,而是繼續往下執行即可,該 function 會等到它被處理完成後反響回來。

圖片來源: https://stackoverflow.com/questions/4844637/what-is-the-difference-between-concurrency-parallelism-and-asynchronous-methods

小總結

在這次的研讀內容裡,其實還涉及到一些概念需要深化,例如過程中需要區分一下 Concurrency vs Parallelism 以及 Async vs Sync 的差異、Javascript 直譯器的運作等等。那麼最後總結幾個小點作為收尾吧!

  1. JS 是單執行緒,但可搭配瀏覽器 WebAPIs 和 Event Loop 的機制達到非同步的效果
  2. JS Engine 裡 Stack 的運作原理,先進後出,被呼叫到的 function 會疊到 Stack 上方,執行完成就拋出,直到 Stack 清空
  3. Callback Queue 是先進先出,WebAPIs 會將 callback function 丟入 Queue 中,在 Stack 清空時將 Queue 的 function 丟入 Stack 中執行
  4. 實作面:在涉及 render 頁面時,要考慮耗時的 function 的位置要設計在 stack 還是交由 WebAPIs。

Reference

https://blog.techbridge.cc/2019/10/05/javascript-async-sync-and-callback/

https://medium.com/javascript-in-plain-english/understanding-javascript-heap-stack-event-loops-and-callback-queue-6fdec3cfe32e

Understanding JavaScript The Weird Part 學習筆記

https://medium.com/@brianwu291/javascript%E5%AD%B8%E7%BF%92%E7%AD%86%E8%A8%98-understanding-javascript-the-weird-part-1-execution-context-lexical-environment-55aebb71222d

Excution Context

http://andyyou.github.io/2015/04/18/what-is-the-execution-context-in-javascript/